Convert `FileFormatModel` from `ABC` to `typing.Protocol` (#3692)

Closes #3100 

## Rationale for this change

`FileFormatModel` had only abstract methods (no shared implementation),
so an `ABC` was heavier than needed. This PR converts it to
`typing.Protocol`, which is the more Pythonic pattern for a pure
interface. `FileFormatWriter` remains an `ABC` since it has shared
`__enter__`/`__exit__`/`result` implementations that inheritance carries
for free. `FileFormatFactory` is unchanged - it matches the existing
`AuthManagerFactory` pattern.

## Changes

New format implementations can now conform structurally without
inheriting from `FileFormatModel`. Net: 2 `ABC`s → 1 `ABC` + 1
`Protocol`. No signature change for existing callers;
`ParquetFormatModel(FileFormatModel)` inheritance is retained
(`Protocol` supports explicit conformance).

## Are these changes tested?

Yes. `tests/io/test_fileformat.py` adds two tests using a shared
`_StructuralModel` helper:

- `test_file_format_model_is_protocol` - a structurally-conforming class
(no inheritance) passes `isinstance()` against `FileFormatModel`.
- `test_structural_model_works_with_factory` - a structurally-conforming
class can be registered and retrieved via `FileFormatFactory`
end-to-end.

## Are there any user-facing changes?

No. Default behavior is unchanged.
diff --git a/pyiceberg/io/fileformat.py b/pyiceberg/io/fileformat.py
index 337e698..65c0cbe 100644
--- a/pyiceberg/io/fileformat.py
+++ b/pyiceberg/io/fileformat.py
@@ -21,7 +21,7 @@
 
 from abc import ABC, abstractmethod
 from dataclasses import dataclass
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
 
 from pyiceberg.io import OutputFile
 from pyiceberg.manifest import FileFormat
@@ -143,18 +143,17 @@
         self._result = self.close()
 
 
-class FileFormatModel(ABC):
+@runtime_checkable
+class FileFormatModel(Protocol):
     """Represents a file format's capabilities. Creates writers."""
 
     @property
-    @abstractmethod
     def format(self) -> FileFormat: ...
 
-    @abstractmethod
     def file_extension(self) -> str:
         """Return file extension without dot, e.g. 'parquet', 'orc'."""
+        ...
 
-    @abstractmethod
     def create_writer(
         self,
         output_file: OutputFile,
@@ -162,9 +161,9 @@
         properties: Properties,
     ) -> FileFormatWriter: ...
 
-    @abstractmethod
     def add_field_metadata(self, field: NestedField, metadata: dict[bytes, bytes], include_field_ids: bool) -> None:
         """Add format-specific Arrow field metadata."""
+        ...
 
 
 class FileFormatFactory:
diff --git a/tests/io/test_fileformat.py b/tests/io/test_fileformat.py
index 328fee9..d5d487f 100644
--- a/tests/io/test_fileformat.py
+++ b/tests/io/test_fileformat.py
@@ -77,3 +77,36 @@
     writer = _DummyWriter()
     with pytest.raises(RuntimeError, match="Writer has not been closed yet"):
         writer.result()
+
+
+class _StructuralModel:
+    """Non-inheriting class that structurally conforms to the FileFormatModel Protocol."""
+
+    @property
+    def format(self) -> FileFormat:
+        return FileFormat.ORC
+
+    def file_extension(self) -> str:
+        return "orc"
+
+    def create_writer(self, output_file: Any, file_schema: Any, properties: Any) -> Any:
+        raise NotImplementedError
+
+    def add_field_metadata(self, field: Any, metadata: Any, include_field_ids: bool) -> None:
+        pass
+
+
+def test_file_format_model_is_protocol() -> None:
+    """A structurally-conforming class (no inheritance) passes isinstance() against FileFormatModel."""
+    assert isinstance(_StructuralModel(), FileFormatModel)
+
+
+def test_structural_model_works_with_factory() -> None:
+    """A structurally-conforming class (no inheritance) can be registered and retrieved via FileFormatFactory."""
+    original = dict(FileFormatFactory._registry)
+    try:
+        model = _StructuralModel()
+        FileFormatFactory.register(model)
+        assert FileFormatFactory.get(FileFormat.ORC) is model
+    finally:
+        FileFormatFactory._registry = original