See below my little demo and explanation of how this can be achieved.
This is my main scene structure:
This is the bullet scene structure:
This is the script on the Player node:
extends Node3D
@export var camera: Camera3D
@export var bullet_scene: PackedScene
var new_bullet: RigidBody3D
var shooting_interval: float = 0.05
var shooting_direction: Vector3 = Vector3.FORWARD
var shooting_force: float = 100.0
var max_angle_deviation: float = 5.0
func _ready() -> void:
	await get_tree().create_timer(2.0).timeout
	var tween: Tween = create_tween().set_loops()
	tween.tween_callback(shoot)
	tween.tween_interval(shooting_interval)
func shoot() -> void:
	var horizontal_angle_deviation = randf_range(-deg_to_rad(max_angle_deviation), deg_to_rad(max_angle_deviation))
	var random_rotation = randf_range(0.0, TAU)
	var shooting_direction_with_deviation = shooting_direction.rotated(Vector3.UP.rotated(Vector3.BACK, random_rotation), horizontal_angle_deviation).normalized()
	
	new_bullet = bullet_scene.instantiate()
	get_tree().current_scene.add_child(new_bullet)
	new_bullet.global_position = camera.global_position
	new_bullet.global_position.y = camera.global_position.y / 2
	new_bullet.apply_impulse(shooting_direction_with_deviation * shooting_force)
This is the script on the Bullet node:
extends RigidBody3D
@export var collision_shape: CollisionShape3D
func _ready() -> void:
	body_entered.connect(_on_body_entered)
func _on_body_entered(_body: Node) -> void:
	disable_collisions.call_deferred()
func disable_collisions() -> void:
	collision_shape.disabled = true
	freeze = true
How this works is that the initial shooting_direction vector (Vector3.FORWARD) is rotated horizontally within the ± max_angle_deviation restrictions around a random axis calculated with the random_rotation angle.
The rest of the code is just boiler plate code for the demo video.
You might still need to adjust it to fit your project, but maybe this will nudge you into the correct direction.

